#include "FileSystem.h"
#include "File.h"
#include <iostream>
#include <sstream>
#include <iomanip>
#include "Context.h"

#ifdef WIN32
#include <iostream>
#include <string>
#include <locale>
#include <codecvt>
#include <fstream>
#include <direntw.h>
#include <direct.h>
#include <windows.h>
#include <shellapi.h>
#include <shlobj.h>
#include <sys/types.h>
#include <sys/utime.h>
#else
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/syslimits.h>
#endif // WIN32

#define MAX_PATH 1024

#ifdef WIN32
std::string FileSystem::Sep = "\\";
std::string gbk_to_utf8(const std::string& str)
{
	const char* GBK_LOCALE_NAME = ".936";
	std::wstring_convert<std::codecvt_byname<wchar_t, char, mbstate_t>> convert(new std::codecvt_byname<wchar_t, char, mbstate_t>(GBK_LOCALE_NAME));
	std::wstring tmp_wstr = convert.from_bytes(str);

	std::wstring_convert<std::codecvt_utf8<wchar_t>> cv2;
	return cv2.to_bytes(tmp_wstr);
}
#else
std::string FileSystem::Sep = "/";
#endif

std::string string_trimmed(std::string& scource)
{
	unsigned trimStart = 0;
	unsigned trimEnd = scource.length();

	while (trimStart < trimEnd)
	{
		char c = scource[trimStart];
		if (c != ' ' && c != 9)
			break;
		++trimStart;
	}
	while (trimEnd > trimStart)
	{
		char c = scource[trimEnd - 1];
		if (c != ' ' && c != 9)
			break;
		--trimEnd;
	}

	return scource.substr(trimStart, trimEnd - trimStart);
}

extern int string_replase(std::string& s1, const std::string& s2, const std::string& s3)
{
	std::string::size_type pos = 0;
	std::string::size_type a = s2.size();
	std::string::size_type b = s3.size();
	while ((pos = s1.find(s2, pos)) != std::string::npos)
	{
		s1.replace(pos, a, s3);
		pos += b;
	}
	return 0;
}

extern std::string GetNativePath(const std::string& pathName)
{
#ifdef _WIN32
	std::string temp(pathName);
	string_replase(temp, "/", "\\");
	return temp;
#else
	return pathName;
#endif
}


std::string AddTrailingSlash(const std::string& pathName)
{
	std::string temp(pathName);
	string_replase(temp, "\\", "/");
	if (temp.length() > 0 && temp.back() != '/')
		temp.push_back('/');
	return temp;
}

std::string RemoveTrailingSlash(const std::string& pathName)
{
	std::string temp(pathName);
	string_replase(temp, "\\", "/");
	if (temp.length() > 0 && temp.back() == '/')
		temp.resize(temp.length() - 1);
	return temp;
}

std::string GetFileName(std::string& path)
{
	std::string temp(path);
	string_replase(temp, "\\", "/");
	unsigned pos = RemoveTrailingSlash(temp).find_last_of('/');
	if (pos != std::string::npos)
		return temp.substr(pos, temp.length() - pos);
	return "";
}

std::string PathJoin(const std::vector<std::string>& paths)
{
	std::string out;
	for (size_t i = 0; i < paths.size(); i++)
	{
		out = out + FileSystem::Sep + paths[i];
	}

	return out;
}

static std::string round_n(double vvalue, int n)
{
	std::stringstream tmp;
	tmp << std::setprecision(n) << std::fixed << vvalue;
	return tmp.str();
}

static void FormatFileSize(size_t vByteSize, std::string* vFormat)
{
	if (vFormat && vByteSize != 0)
	{
		static double lo = 1024;
		static double ko = 1024 * 1024;
		static double mo = 1024 * 1024 * 1024;

		double v = (double)vByteSize;

		if (v < lo)
			*vFormat = round_n(v, 0) + " o"; // octet
		else if (v < ko)
			*vFormat = round_n(v / lo, 2) + " Ko"; // ko
		else  if (v < mo)
			*vFormat = round_n(v / ko, 2) + " Mo"; // Mo 
		else
			*vFormat = round_n(v / mo, 2) + " Go"; // Go 
	}
}

FileSystem::FileSystem(Context* context):context_(context)
{
	addSearchePath("");
}

FileSystem::~FileSystem()
{
}

bool FileSystem::FileExists(const std::string& fileName) const
{
	FILE* pFile;
	pFile = fopen(fileName.c_str(), "r");
	if (pFile != NULL)
	{
		fclose(pFile);
		return true;
	}
	return false;
}

void FileSystem::addSearchePath(std::string path)
{
	allowedPaths_.push_back(AddTrailingSlash(path));
}

inline int alphaSort(const struct dirent** a, const struct dirent** b)
{
	return strcoll((*a)->d_name, (*b)->d_name);
}

std::string FileSystem::OptimizeToLow(std::string vFileName)
{
	// convert to lower case
	for (char& c : vFileName)
		c = c - 'A' + 'a';
	return vFileName;
}

void FileSystem::FillInfos(FileInfoStruct* vFileInfoStruct)
{
	if (vFileInfoStruct && vFileInfoStruct->fileName != "..")
	{
		// _stat struct :
		//dev_t     st_dev;     /* ID of device containing file */
		//ino_t     st_ino;     /* inode number */
		//mode_t    st_mode;    /* protection */
		//nlink_t   st_nlink;   /* number of hard links */
		//uid_t     st_uid;     /* user ID of owner */
		//gid_t     st_gid;     /* group ID of owner */
		//dev_t     st_rdev;    /* device ID (if special file) */
		//off_t     st_size;    /* total size, in bytes */
		//blksize_t st_blksize; /* blocksize for file system I/O */
		//blkcnt_t  st_blocks;  /* number of 512B blocks allocated */
		//time_t    st_atime;   /* time of last access - not sure out of ntfs */
		//time_t    st_mtime;   /* time of last modification - not sure out of ntfs */
		//time_t    st_ctime;   /* time of last status change - not sure out of ntfs */

		std::string fpn;

		if (vFileInfoStruct->type == 'f') // file
			fpn = vFileInfoStruct->filePath + Sep + vFileInfoStruct->fileName;
		else if (vFileInfoStruct->type == 'l') // link
			fpn = vFileInfoStruct->filePath + Sep + vFileInfoStruct->fileName;
		else if (vFileInfoStruct->type == 'd') // directory
			fpn = vFileInfoStruct->filePath + Sep + vFileInfoStruct->fileName;

		struct stat statInfos;
		char timebuf[100];
		int result = stat(fpn.c_str(), &statInfos);
		if (!result)
		{
			if (vFileInfoStruct->type != 'd')
			{
				vFileInfoStruct->fileSize = (size_t)statInfos.st_size;
				FormatFileSize(vFileInfoStruct->fileSize,
					&vFileInfoStruct->formatedFileSize);
			}

			size_t len = 0;
#ifdef MSVC
			struct tm _tm;
			errno_t err = localtime_s(&_tm, &statInfos.st_mtime);
			if (!err) len = strftime(timebuf, 99, "%Y/%m/%d ", &_tm);
#else
			struct tm* _tm = localtime(&statInfos.st_mtime);
			if (_tm) len = strftime(timebuf, 99, "%Y/%m/%d ", _tm);
#endif
			if (len)
			{
				vFileInfoStruct->fileModifDate = std::string(timebuf, len);
			}
		}
	}
}

void FileSystem::ListFiles(const char* dir, std::vector<FileInfoStruct>& listOut)
{
	ListFiles(dir, [&](const FileInfoStruct& info) {
		listOut.emplace_back(info);
	});
}

void FileSystem::ListFiles(const char* dir, std::function<void(const FileInfoStruct&)> handler)
{
	if (!handler)
	{
		return;
	}

	auto truepath = GetNativePath(dir);
	struct dirent** files = nullptr;
	int i = 0;
	int n = 0;
	n = scandir(truepath.c_str(), &files, nullptr, alphaSort);
	if (n > 0)
	{
		for (i = 0; i < n; i++)
		{
			struct dirent* ent = files[i];

			FileInfoStruct infos;

			infos.filePath = dir;
			infos.fileName = (ent->d_name);
			infos.fileName_optimized = OptimizeToLow(infos.fileName);

			if (("." != infos.fileName))
			{
				switch (ent->d_type)
				{
				case DT_REG:
					infos.type = 'f'; break;
				case DT_DIR:
					infos.type = 'd'; break;
				case DT_LNK:
					infos.type = 'l'; break;
				}

				if (infos.type == 'f' ||
					infos.type == 'l') // link can have the same extention of a file
				{
					size_t lpt = infos.fileName.find_last_of('.');
					if (lpt != std::string::npos)
					{
						infos.ext = infos.fileName.substr(lpt);
					}
				}

				FillInfos(&infos);
				handler(infos);
			}
		}

		for (i = 0; i < n; i++)
		{
			free(files[i]);
		}

		free(files);
	}
}

void FileSystem::ListRelativeFiles(const char* path, std::vector<FileInfoStruct>& listOut)
{
	for (auto searchPath : allowedPaths_)
	{
		auto truepath = GetNativePath(searchPath + path);
		ListFiles(truepath.c_str(), listOut);
	}
}

std::shared_ptr<File> FileSystem::GetFile(const std::string& path, bool fullPath)
{
	if (fullPath)
	{
		auto truepath = GetNativePath(path);
		if (FileExists(truepath))
		{
			std::shared_ptr<File> file(new File(truepath.c_str(), FILE_READ));
			std::cout << truepath << std::endl;
			return file;
		}
		return nullptr;
	}

	for (auto searchPath : allowedPaths_)
	{
		auto truepath = GetNativePath(searchPath + path);
		if (FileExists(truepath))
		{
			std::shared_ptr<File> file(new File(truepath.c_str(), FILE_READ));
			std::cout << truepath << std::endl;
			return file;
		}
	}
	return nullptr;
}


std::string FileSystem::GetCurrentDir()
{
	std::string path = ".";
	DIR* dir = opendir(path.c_str());
	char  real_path[PATH_MAX];

	if (nullptr == dir)
	{
		path = ".";
		dir = opendir(path.c_str());
	}

	if (nullptr != dir)
	{
#ifdef WIN32
		size_t numchar = GetFullPathNameA(path.c_str(), PATH_MAX - 1, real_path, nullptr);
#else
		char* numchar = realpath(path.c_str(), real_path);
#endif
		closedir(dir);
	}

	return std::move(std::string(real_path));
}


std::string FileSystem::GetUserDocumentsDir() const
{
#if defined(_WIN32)
	char pathName[MAX_PATH];
	pathName[0] = 0;
	SHGetSpecialFolderPath(nullptr, pathName, CSIDL_PERSONAL, 0);
	std::string utf8_name = gbk_to_utf8(pathName);
	return AddTrailingSlash(utf8_name);
#else
	char pathName[MAX_PATH];
	pathName[0] = 0;
	strcpy(pathName, getenv("HOME"));
	return AddTrailingSlash(std::string(pathName));
#endif
}

std::string GetParentPath(const std::string& path)
{
	unsigned pos = RemoveTrailingSlash(path).find_last_of('/');
	if (pos != std::string::npos)
		return path.substr(0, pos + 1);
	else
		return std::string();
}

bool FileSystem::CreateDir(std::string path)
{
	// Create each of the parents if necessary
	std::string parentPath = GetParentPath(path);
	if (parentPath.length() > 1 && !DirExists(parentPath))
	{
		if (!CreateDir(parentPath))
			return false;
	}

#ifndef _WIN32
	bool success = mkdir(GetNativePath(RemoveTrailingSlash(path)).c_str(), 0777) == 0 || errno == EEXIST;
#else
	bool success = mkdir(GetNativePath(RemoveTrailingSlash(path)).c_str()) == 0 || errno == EEXIST;
#endif

	if (success)
		std::cout << ("Created directory " + path) << std::endl;
	else
		std::cout << ("Failed to create directory " + path) << std::endl;

	return success;
}

bool FileSystem::DirExists(const std::string& pathName) const
{
#ifndef _WIN32
	// Always return true for the root directory
	if (pathName == "/")
		return true;
#endif

	std::string fixedName = GetNativePath(RemoveTrailingSlash(pathName));

	struct stat st {};
	if (stat(fixedName.c_str(), &st) || !(st.st_mode & S_IFDIR))
		return false;

	return true;
}

